﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;

namespace TestRunner 
{
    class MainViewModel : ModelBase
    {
        private bool testRunning;
        private TestRunner runner;
        private readonly List<string> assemblyPaths = new List<string>();
        private bool runningInLoops;
        private int repeationCount;

        private readonly List<TestEntry> testDetails = new List<TestEntry>();
        private readonly Dictionary<TestViewModel, TestEntry> vmToModelMap = new Dictionary<TestViewModel, TestEntry>();
        private readonly Dictionary<TestEntry, TestViewModel> modelToVmMap = new Dictionary<TestEntry, TestViewModel>();
        private readonly HashSet<TestViewModel>  testsInRightPane = new HashSet<TestViewModel>();
        private readonly List<GenericCommand> commandsRelatedToRunState = new List<GenericCommand>();

        public ObservableCollection<TestViewModel> Results { get; set; }
        public ObservableCollection<ITestViewModel> TestViewModels { get; set; }

        public GenericCommand MarkSelectedTestAsIndependent { get; private set; }
        public GenericCommand UnMarkSelectedTestAsIndependent { get; private set; }

        public GenericCommand RunTestsIndividuallyCommand { get; private set; }
        public GenericCommand RunTestsWithParallelismCommand { get; private set; }
        public GenericCommand RunTestsMulitpleTimesCommand { get; private set; }
        public GenericCommand StopTestsCommand { get; private set; }
        public GenericCommand ForceStopTestsCommand { get; private set; }

        public ICommand OpenFileCommand { get; private set; }

        public event EventHandler ConfigureTools;
        public event EventHandler<ShowCountDialogEventArgs> ShowCountDialog;
        public event EventHandler TestStarting;
        private ITestViewModel selectedItem;
        public ITestViewModel SelectedItem
        {
            get { return selectedItem; }
            set
            {
                selectedItem = value;
                NotifyPropertyChanged<MainViewModel>(p => p.SelectedItem);
                NotifyPropertyChanged<MainViewModel>(p => p.CanMarkIndependent);
                NotifyPropertyChanged<MainViewModel>(p => p.CanUnMarkIndependent);
            }
        }
        
        public bool RunningInLoops
        {
            get { return runningInLoops; }
            set
            {
                if (runningInLoops != value)
                {
                    runningInLoops = value;
                    NotifyPropertyChanged<MainViewModel>(p => p.RunningInLoops);
                    NotifyPropertyChanged<MainViewModel>(p => p.NotRunningInLoops);
                }
            }
        }

        public bool NotRunningInLoops
        {
            get { return !runningInLoops; }
        }

        public bool CanMarkIndependent 
        {
            get
            {
                if (SelectedItem is NameSpaceViewModel)
                {
                    return true;
                }
                TestViewModel tvm = SelectedItem as TestViewModel;
                if (tvm != null && tvm.CanRunIndependently == false)
                {
                    return true;
                }
                return false;
            }
        }

        public bool CanUnMarkIndependent 
        {
            get
            {
                if (SelectedItem is NameSpaceViewModel)
                {
                    return true;
                }
                TestViewModel tvm = SelectedItem as TestViewModel;
                if (tvm != null && tvm.CanRunIndependently)
                {
                    return true;
                }
                return false;
            }
        }

        public bool AllowSelectionChange { get { return TestRunning == false; } }
        public List<CommandBinding> CommandBindings { get; set; }

        public bool TestRunning
        {
            get { return testRunning; }
            set
            {
                if (testRunning != value)
                {
                    testRunning = value;
                    NotifyPropertyChanged<MainViewModel>(p => p.TestRunning);
                }
            }
        }

        private bool showNunitConsoleOutput = true;
        public bool ShowNunitConsoleOutput
        {
            get { return showNunitConsoleOutput; }
            set 
            { 
                showNunitConsoleOutput = value;
                NotifyPropertyChanged("ShowNunitConsoleOutput");
            }
        }

        public bool collateTests;
        public bool CollateTests { 
            get { return collateTests; } 
            set
            {
                collateTests = value;
                NotifyPropertyChanged("CollateTests");
            } 
        }

        private readonly LocalSettings settings;
        private readonly TextWriter outputStream;
        private readonly Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
        
        public MainViewModel(LocalSettings settings, TextWriter outputStream)
        {
            this.settings = settings;
            this.outputStream = outputStream;
            CommandBindings = new List<CommandBinding>();
            Results = new ObservableCollection<TestViewModel>();
            TestViewModels = new ObservableCollection<ITestViewModel>();

            
            CommandBinding closeCommandBinding = new CommandBinding(ApplicationCommands.Close, OnCloseFiles, CanOpenCloseFile);
            CommandBindings.Add(closeCommandBinding);

            MarkSelectedTestAsIndependent = new GenericCommand(OnMarkSelectedTestAsIndependent, CanRunTests);
            UnMarkSelectedTestAsIndependent = new GenericCommand(OnUnMarkSelectedTestAsIndependent, CanRunTests);

            RunTestsWithParallelismCommand = new GenericCommand(OnRunTestWithParallelism, CanRunTests);
            RunTestsIndividuallyCommand = new GenericCommand(OnRunTestIndividually, CanRunTests);
            RunTestsMulitpleTimesCommand = new GenericCommand(OnRunTestMultipleTimes, CanRunTests);
            StopTestsCommand = new GenericCommand(OnStopTests, CanStopTests);
            ForceStopTestsCommand = new GenericCommand(OnForceStopTests, CanStopTests);

            CommandBindings.Add(new CommandBinding(RunTestsWithParallelismCommand));
            CommandBindings.Add(new CommandBinding(RunTestsIndividuallyCommand));
            CommandBindings.Add(new CommandBinding(RunTestsMulitpleTimesCommand));
            CommandBindings.Add(new CommandBinding(StopTestsCommand));
            CommandBindings.Add(new CommandBinding(ForceStopTestsCommand));
            CommandBindings.Add(new CommandBinding(OpenFileCommand = new OpenFileCommand()));
            CommandBindings.Add(new CommandBinding(MarkSelectedTestAsIndependent));
            CommandBindings.Add(new CommandBinding(UnMarkSelectedTestAsIndependent));

            commandsRelatedToRunState.Add(MarkSelectedTestAsIndependent);
            commandsRelatedToRunState.Add(UnMarkSelectedTestAsIndependent);

            commandsRelatedToRunState.Add(RunTestsWithParallelismCommand);
            commandsRelatedToRunState.Add(RunTestsIndividuallyCommand);
            commandsRelatedToRunState.Add(RunTestsMulitpleTimesCommand);
            commandsRelatedToRunState.Add(StopTestsCommand);
            commandsRelatedToRunState.Add(ForceStopTestsCommand);
        }
        

        internal void CanOpenCloseFile(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = TestRunning == false;
        }

        private void OnCloseFiles(object sender, ExecutedRoutedEventArgs e)
        {
            UpdateSettings();

            assemblyPaths.Clear();
            Results.Clear();
            TestViewModels.Clear();
            vmToModelMap.Clear();
            modelToVmMap.Clear();
            testDetails.Clear();
            testsInRightPane.Clear();

            if (outputStream != null)
            {
                outputStream.Flush();
            }
        }
        
        internal void UpdateSettings()
        {
            if (runner != null) runner.Stop();

            foreach (var assemblyPath in assemblyPaths)
            {
                if (settings.TestMap.ContainsKey(assemblyPath))
                {
                    settings.TestMap.Remove(assemblyPath);
                }
            }

            foreach (var testViewModel in TestViewModels)
            {
                AddSettingData(testViewModel);
            }

            settings.LastUsedAssemblyPath = assemblyPaths.Distinct().ToList();
        }

        void AddSettingData(ITestViewModel vm)
        {
            TestViewModel tvm = vm as TestViewModel;

            if (tvm == null)
            {
                if (vm.Children != null)
                {
                    foreach (var test in vm.Children)
                    {
                        AddSettingData(test);
                    }
                }
            }
            else
            {
                Tuple<HashSet<string>, HashSet<string>> previousDetails;
                HashSet<string> selectedTests;
                HashSet<string> independentTests;

                if (!settings.TestMap.TryGetValue(tvm.Assembly, out previousDetails))
                {
                    selectedTests = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                    independentTests = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                    settings.TestMap[tvm.Assembly] = new Tuple<HashSet<string>, HashSet<string>>(selectedTests, independentTests);
                }
                else
                {
                    selectedTests = previousDetails.Item1;
                    independentTests = previousDetails.Item2;
                }
                
                if (independentTests != null && tvm.CanRunIndependently) 
                {
                    independentTests.Add(tvm.FullName);
                }

                if (selectedTests != null && tvm.IsChecked.HasValue && tvm.IsChecked.Value) 
                {
                    selectedTests.Add(tvm.FullName);
                }
            }
        }

        
        void NotifyTestRunStateChanged()
        {
            NotifyPropertyChanged<MainViewModel>(p => p.AllowSelectionChange);
            foreach (var genericCommand in commandsRelatedToRunState)
            {
                genericCommand.NotifyStateChange();
            }
        }

        internal void LoadDefaultAsseblyList()
        {
            foreach (var assemblyPath in settings.LastUsedAssemblyPath)
            {
                if (!String.IsNullOrWhiteSpace(assemblyPath) && File.Exists(assemblyPath)) 
                {
                    LoadTests(assemblyPath);
                }
            }
        }
        internal void LoadTests(string assemblyPath)
        {
            testDetails.Clear();
            if (String.IsNullOrWhiteSpace(assemblyPath)) return;
            List<string> testNames;
            try
            {
                testNames = TestDiscoveryHelper.GetTestsInAssembly(assemblyPath, settings.NUnitFolderPath);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                return;
            }
            List<TestEntry> testModels = new List<TestEntry>();

            foreach (var name in testNames)
            {
                int pos = name.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase);
                if (pos < 0) continue;
                string nameSpace = name.Substring(0, pos);
                string className = name.Substring(pos + 1);
                var testEntry = new TestEntry { Assembly = assemblyPath, NameSpace = nameSpace, ClassName = className, Selected = true };
                testEntry.FullName = name;
                testModels.Add(testEntry);
                testDetails.Add(testEntry);
            }

            CreateViewModel(assemblyPath, testModels);
            
            assemblyPaths.Add(assemblyPath);
        }

        private void CreateViewModel(string assemblyName, IEnumerable<TestEntry> testModels) 
        {
            Tuple<HashSet<string>, HashSet<string>> previousDetails;
            HashSet<string> selectedTests = null;
            HashSet<string> independentTests = null;

            settings.TestMap.TryGetValue(assemblyName, out previousDetails);
            if (previousDetails != null) 
            {
                selectedTests = previousDetails.Item1;
                independentTests = previousDetails.Item2;
            }

            NameSpaceViewModel assemblyVm = new NameSpaceViewModel(assemblyName, null);
            TestViewModels.Add(assemblyVm);

            Dictionary<string, NameSpaceViewModel> nameSpaceMap = new Dictionary<string, NameSpaceViewModel>();
            Dictionary<string, TestViewModel> testNameToVmMap = new Dictionary<string, TestViewModel>(StringComparer.OrdinalIgnoreCase);

            foreach (var testDetail in testModels)
            {
                NameSpaceViewModel nameSpaceVm;
                if (!nameSpaceMap.TryGetValue(testDetail.NameSpace, out nameSpaceVm))
                {
                    nameSpaceVm = new NameSpaceViewModel(testDetail.NameSpace, null);
                    nameSpaceMap[testDetail.NameSpace] = nameSpaceVm;
                    assemblyVm.Children.Add(nameSpaceVm);
                }

                var testVm = new TestViewModel(testDetail, nameSpaceVm);
                nameSpaceVm.Children.Add(testVm);
                vmToModelMap[testVm] = testDetail;
                modelToVmMap[testDetail] = testVm;
                testNameToVmMap[testVm.FullName] = testVm;
            }

            if (selectedTests != null)
            {
                foreach (var selectedTest in selectedTests)
                {
                    TestViewModel vm;
                    if (!testNameToVmMap.TryGetValue(selectedTest, out vm)) continue;
                    vm.IsChecked = true;
                }
            }

            if (independentTests != null)
            {
                foreach (var test in independentTests)
                {
                    TestViewModel vm;
                    if (!testNameToVmMap.TryGetValue(test, out vm)) continue;
                    vm.CanRunIndependently = true;
                }
            }
        }
        

        List<TestEntry> GetSelectedTestsAndClearState()
        {
            testsInRightPane.Clear();
            Results.Clear();
            List<TestEntry> tests = new List<TestEntry>();
            foreach (var testViewModel in TestViewModels)
            {
                testViewModel.State = RunState.NotExecuted;
                GetSelectedTestsAndClearState(testViewModel, tests);
            }
            return tests;
        }

        void GetSelectedTestsAndClearState(ITestViewModel testViewModel, List<TestEntry> selection)
        {
            foreach (var testItem in testViewModel.Children)
            {
                testItem.State = RunState.NotExecuted;
                if (testItem is NameSpaceViewModel)
                {
                    GetSelectedTestsAndClearState(testItem, selection);
                }
                else if (testItem is TestViewModel && testItem.IsChecked.HasValue && testItem.IsChecked.Value)
                {
                    TestViewModel tvm = (TestViewModel) testItem;
                    tvm.RunResults.Clear();
                    selection.Add(tvm.TestEntry);
                }
            }
        }        

        private void OnRunTestWithParallelism()
        {
            ShowCountDialogEventArgs args = new ShowCountDialogEventArgs();
            args.Line1Text = "Select the number of parallel threads you want to use";
            args.Line2Text = "Thread count";
            args.Title = "Parallel execution";
            //dlg.Owner = this;
            args.Count = settings.LastSelectedParallelThreads;
            ShowCountDialog(this, args);
            if (args.Success)
            {
                settings.LastSelectedParallelThreads = args.Count;
                RunTests(args.Count, 1);
            }
        }
        
        private void OnRunTestMultipleTimes()
        {
            ShowCountDialogEventArgs args = new ShowCountDialogEventArgs();
            args.Line1Text = "Select the number of times you want to execute each test";
            args.Line2Text = "Count";
            args.Title = "Run Tests Multiple Times";
            args.Count = settings.LastSelectedRepeateCount;
            ShowCountDialog(this, args);
            if (args.Success)
            {
                settings.LastSelectedRepeateCount = args.Count;
                RunTests(1, args.Count);
            }
        }
        
        private void OnRunTestIndividually()
        {
            RunTests(1, 1);
        }
        
        private void RunTests(int parallesimCount, int repeateCount)
        {
            if (outputStream != null)
            {
                outputStream.Flush();
            }
            if (runner != null)
            {
                runner.TestStarted -= runner_TestStarted;
                runner.TestCompleted -= runner_TestCompleted;
                runner.TestsCompleted -= runner_TestsCompleted;
            }
            string nunitExe = settings.GetNunitExecutablePath();
            if (nunitExe == null && ConfigureTools != null)
            {
                ConfigureTools(this, EventArgs.Empty);
            }
            nunitExe = settings.GetNunitExecutablePath();
            if (nunitExe == null) return;

            runner = new TestRunner(nunitExe);

            var selectedTests = GetSelectedTestsAndClearState();
            if (selectedTests.Count == 0) return;
            
            RunningInLoops = repeateCount > 1;
            repeationCount = repeateCount;
            TestRunning = true;
            NotifyTestRunStateChanged();

            
            runner.TestStarted += runner_TestStarted;
            runner.TestCompleted += runner_TestCompleted;
            runner.TestsCompleted += runner_TestsCompleted;
            runner.RunTestAsync(selectedTests, parallesimCount, repeateCount, outputStream, ShowNunitConsoleOutput, CollateTests);
        }

        void runner_TestStarted(object sender, RunStartedEventArgs e)
        {
            Action updateToRunState = () => {

                var vm = modelToVmMap[e.TestEntry];
                vm.State = RunState.Running;

                var handler = TestStarting;
                if (handler != null)
                {
                    handler(this, EventArgs.Empty);
                }
            };
            dispatcher.Invoke(updateToRunState);
        }

        void runner_TestsCompleted(object sender, EventArgs e)
        {
            runner.TestStarted -= runner_TestStarted;
            runner.TestCompleted -= runner_TestCompleted;
            runner.TestsCompleted -= runner_TestsCompleted;

            TestRunning = false;
            dispatcher.Invoke((Action)NotifyTestRunStateChanged);
        }

        void runner_TestCompleted(object sender, RunCompletedEventArgs e)
        {
            var testVm = modelToVmMap[e.TestEntry];
            string resultString = "";
            RunState runState = RunState.NotExecuted;
            resultString = GetResultInformation(e, resultString, ref runState);

            if (RunningInLoops == false)
            {
                testVm.Result = resultString;
                testVm.State = runState;
                testVm.LogPath = e.LogFilePath;
                testVm.ResultPath = e.ResultFilePath;
            }
            else
            {
                RunResultViewModel vm = new RunResultViewModel();
                vm.State = runState;
                vm.Result = resultString;
                vm.LogPath = e.LogFilePath;
                vm.RunNumber = e.RunNumber;
                vm.ResultPath = e.ResultFilePath;
                
                testVm.State = vm.State;
                
                Action addAction1 = () => testVm.RunResults.Add(vm);
                dispatcher.Invoke(addAction1);

                if (repeationCount == testVm.RunResults.Count)
                {
                    Action action2 = () =>
                    {
                        if (repeationCount == testVm.RunResults.Count)
                        {
                            int passedCount = 0;
                            int failedCount = 0;
                            int abortedCount = 0;
                            foreach (var r in testVm.RunResults)
                            {
                                if (r.State == RunState.Passed) passedCount++;
                                if (r.State == RunState.Failed) failedCount++;
                                if (r.State == RunState.Aborted) abortedCount++;
                            }
                            if (repeationCount == passedCount)
                            {
                                testVm.State = RunState.Passed;
                                testVm.Result = "Passed";

                            }
                            else if (abortedCount > 0)
                            {
                                testVm.State = RunState.Aborted;
                                testVm.Result = "Aborted";
                            }
                            else if(failedCount >0)
                            {
                                testVm.State = RunState.Failed;
                                testVm.Result = "Failed";
                            }
                        }
                    };
                    
                    dispatcher.Invoke(action2);
                }
            }

            lock (testsInRightPane)
            {
                if (!testsInRightPane.Contains(testVm))
                {
                    Action addAction = () => Results.Add(testVm);
                    dispatcher.Invoke(addAction);
                    testsInRightPane.Add(testVm);
                }
            }
        }

        private static string GetResultInformation(RunCompletedEventArgs e, string resultString, ref RunState runState)
        {
            if (e.ErrorMessage == null)
            {
                if (e.ExitCode == 0)
                {
                    resultString = "Passed";
                    runState = RunState.Passed;
                }
                else if (e.ExitCode < 0)
                {
                    resultString = "Exited With Exit Code: " + e.ExitCode;
                    runState = RunState.Aborted;
                }
                else if (e.ExitCode > 0)
                {
                    resultString = "Test Failed";
                    runState = RunState.Failed;
                }
            }
            else
            {
                resultString = e.ErrorMessage;
                runState = RunState.Aborted;
            }
            return resultString;
        }

        private bool CanRunTests()
        {
            return TestRunning == false;
        }

        private void OnStopTests()
        {
            runner.Stop();
        }

        private void OnForceStopTests() 
        {
            runner.Stop();
            AshuSoft.ParentProcessUtilities.KillProcessChild(Process.GetCurrentProcess().Id, "nunit");
        }
       
        private bool CanStopTests()
        {
            return TestRunning;
        }

        private void OnMarkSelectedTestAsIndependent()
        {
            ITestViewModel tvm = SelectedItem;
            if (tvm != null)
            {
                MarkAsIndependent(tvm, true);
            }
        }

        private void OnUnMarkSelectedTestAsIndependent()
        {
            ITestViewModel tvm = SelectedItem;
            if (tvm != null)
            {
                MarkAsIndependent(tvm, false);
            }
        }
        
        void MarkAsIndependent(ITestViewModel itvm, bool canRun)
        {
            if (itvm is TestViewModel)
            {
                ((TestViewModel) itvm).CanRunIndependently = canRun;
                NotifyPropertyChanged<MainViewModel>(p => p.CanMarkIndependent);
                NotifyPropertyChanged<MainViewModel>(p => p.CanUnMarkIndependent);
            }
            else if(itvm.Children != null)
            {
                foreach (var testViewModel in itvm.Children)
                {
                    MarkAsIndependent(testViewModel, canRun);
                }
            }
        }
        
    }

    internal class ShowCountDialogEventArgs :EventArgs
    {
        public int Count { get; set; }
        public string Line1Text { get; set; }
        public string Line2Text { get; set; }
        public string Title { get; set; }

        public bool Success { get; set; }
    }
}
